Skip to content

feat(analytics): count SPA pageviews incl. each opened anchor#562

Merged
rdmueller merged 1 commit into
LLM-Coding:mainfrom
raifdmueller:feat/anchor-pageview-tracking
Jun 2, 2026
Merged

feat(analytics): count SPA pageviews incl. each opened anchor#562
rdmueller merged 1 commit into
LLM-Coding:mainfrom
raifdmueller:feat/anchor-pageview-tracking

Conversation

@raifdmueller
Copy link
Copy Markdown
Contributor

@raifdmueller raifdmueller commented Jun 2, 2026

Why

Follow-up to #559. GoatCounter's count.js only fires on the initial full page load, so the stats only showed entry pages — in-app navigation (opening anchors, switching to /contracts, /about, docs) wasn't counted. In particular we couldn't see which anchors users are interested in.

What

Report a GoatCounter pageview from the router on every client-side route change (handleRoute), with the resolved path and title:

  • Each opened anchor /anchor/:id is now counted with its readable title → per-anchor view counts.
  • Doc/sub-pages reached by in-app navigation are counted too.
  • The first handleRoute() is skipped (the initial load is already auto-counted by count.js), so no double-counting.
  • Guarded (window.goatcounter?.count), so nothing breaks if the script is blocked/not loaded.

Privacy unchanged: path only (no query string), no personal data — same as the existing path/referrer config.

Verification

  • Unit test: a SPA navigation reports goatcounter.count({ path: … }) (router.test.js).
  • Preview build, spying on goatcounter.count:
    • /anchor/conways-law{ path: "/Semantic-Anchors/anchor/conways-law", title: "Conways Law — Semantic Anchors" }
    • /contracts{ path: "/Semantic-Anchors/contracts", title: "Semantic Contracts — Semantic Anchors" }
  • All unit tests pass; lint + prettier clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Neue Funktionen

    • Verbesserte Verfolgung von Seitenaufrufen bei Client-seitiger Navigation für detailliertere Website-Analytik
  • Tests

    • Tests zur Verifizierung der korrekten Ausführung der Seitenaufrufen-Nachverfolgung hinzugefügt

GoatCounter's count.js only fires on the initial full page load, so
client-side navigation (opening anchors, switching to docs/contracts) was
not counted — the stats only showed entry pages. Report a pageview from
the router on every client-side route change, skipping the first
handleRoute() so the initial load (already auto-counted by count.js) is
not double-counted.

Each opened anchor (/anchor/:id) is now recorded with its path and
readable title, giving per-anchor view counts. Privacy is unchanged:
path only (no query string), no personal data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Walkthrough

Der Router wird um GoatCounter-Integration für Pageview-Tracking erweitert. Eine neue trackPageview()-Funktion wird eingeführt, die Client-seitige SPA-Navigationen zählt, die erste Navigation überspringt und bei späteren Navigationsereignissen window.goatcounter.count() mit aktuellem Pfad und Titel aufruft.

Changes

GoatCounter-Pageview-Tracking für SPA-Navigation

Layer / Datei(en) Zusammenfassung
TrackPageview-Funktion und Initialisierung
website/src/utils/router.js
Eine neue interne trackPageview()-Hilfsfunktion wird definiert, die durch ein firstRouteHandled-Flag die erste handleRoute()-Ausführung überspringt und bei späteren Navigationen window.goatcounter.count() mit aktuellem pathname und document.title aufruft.
Tracking in Navigationspfaden
website/src/utils/router.js
trackPageview() wird explizit nach dem Setzen des Anchor-Titels im Anchor-Routenpfad und vor der Ausführung des Route-Handlers im regulären Navigationspfad aufgerufen.
Test für GoatCounter-Pageview-Tracking
website/src/utils/router.test.js
Ein neuer Testfall validiert, dass bei SPA-Navigation ein GoatCounter-Pageview mit dem korrekten path-Argument der navigierten Route ausgelöst wird, wobei die erste Navigation übersprungen wird.

Sequence Diagram

sequenceDiagram
  participant User as Benutzer
  participant Router
  participant trackPageview
  participant GoatCounter

  User->>Router: navigiert zur Route
  Router->>trackPageview: ruft trackPageview() auf
  Note over trackPageview: überspringt ersten Aufruf<br/>(firstRouteHandled-Flag)
  trackPageview->>GoatCounter: window.goatcounter.count({path, title})
  GoatCounter-->>trackPageview: Tracking registriert
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Possibly related PRs

  • LLM-Coding/Semantic-Anchors#559: Diese PR nutzt die GoatCounter-Integration (window.goatcounter.count), die durch PR #559 eingeführt wurde.
  • LLM-Coding/Semantic-Anchors#385: Beide PRs ändern handleRoute() so, dass document.title für Pageview-Tracking (window.goatcounter.count mit title und path) gesetzt und genutzt wird.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Der PR-Titel bezieht sich direkt auf die Hauptänderung: Implementierung von GoatCounter-Analytics-Tracking für SPA-Seitenaufrufe, einschließlich Anchor-Navigation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
website/src/utils/router.js (1)

212-220: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fehlender trackPageview()-Aufruf im Fallback-Pfad.

Wenn eine Route nicht gefunden wird und zur Homepage zurückgefallen wird, fehlt der trackPageview()-Aufruf. Dies führt dazu, dass Navigationen zu unbekannten Routes nicht in GoatCounter gezählt werden.

🔧 Vorgeschlagener Fix
   } else {
     // Default to home if route not found
     const homeHandler = routes.get('/')
     if (typeof homeHandler === 'function') {
       currentRoute = '/'
       document.title = ROUTE_TITLES['/']
       homeHandler()
+      trackPageview()
     }
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/utils/router.js` around lines 212 - 220, When falling back to the
homepage in the router (the else branch that gets homeHandler via
routes.get('/')), add a call to trackPageview() after setting currentRoute = '/'
and document.title = ROUTE_TITLES['/'] and before or after invoking
homeHandler(); ensure the trackPageview() invocation is present so unknown-route
navigations are recorded by GoatCounter (refer to currentRoute, ROUTE_TITLES,
homeHandler(), and trackPageview()).
🧹 Nitpick comments (1)
website/src/utils/router.test.js (1)

39-51: ⚡ Quick win

Test-Coverage für title-Feld und explizite Verifikation des Skip-Verhaltens fehlt.

Der Test prüft nur das path-Feld, obwohl laut Implementierung auch title an goatcounter.count() übergeben wird. Außerdem wird nicht explizit verifiziert, dass die erste Navigation tatsächlich übersprungen wird.

🧪 Vorgeschlagene Verbesserung der Test-Abdeckung
-  it('reports a GoatCounter pageview with the path on SPA navigation', () => {
+  it('reports a GoatCounter pageview with path and title on SPA navigation', () => {
     window.goatcounter = { count: vi.fn() }
     addRoute('/gc-test', vi.fn())
     // The very first route of the run is skipped by design (count.js auto-counts
     // the initial load); clear and navigate again so we assert a later change.
     navigate('/gc-test')
-    window.goatcounter.count.mockClear()
+    expect(window.goatcounter.count).not.toHaveBeenCalled()
     navigate('/gc-test')
     expect(window.goatcounter.count).toHaveBeenCalledWith(
-      expect.objectContaining({ path: '/gc-test' })
+      expect.objectContaining({ 
+        path: '/gc-test',
+        title: expect.any(String)
+      })
     )
     delete window.goatcounter
   })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@website/src/utils/router.test.js` around lines 39 - 51, The test only asserts
the path but must also assert the title and that the initial navigation was
skipped: after setting window.goatcounter = { count: vi.fn() } and calling
addRoute('/gc-test', ...), explicitly call navigate('/gc-test') and assert
window.goatcounter.count was not called (verifying the skip behavior), then
perform the second navigate and assert window.goatcounter.count was called with
an objectContaining both { path: '/gc-test', title: 'GC Test' } (or the expected
document/title value used by your router); reference the existing helpers
addRoute and navigate and the mock window.goatcounter.count when adding these
assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@website/src/utils/router.js`:
- Around line 212-220: When falling back to the homepage in the router (the else
branch that gets homeHandler via routes.get('/')), add a call to trackPageview()
after setting currentRoute = '/' and document.title = ROUTE_TITLES['/'] and
before or after invoking homeHandler(); ensure the trackPageview() invocation is
present so unknown-route navigations are recorded by GoatCounter (refer to
currentRoute, ROUTE_TITLES, homeHandler(), and trackPageview()).

---

Nitpick comments:
In `@website/src/utils/router.test.js`:
- Around line 39-51: The test only asserts the path but must also assert the
title and that the initial navigation was skipped: after setting
window.goatcounter = { count: vi.fn() } and calling addRoute('/gc-test', ...),
explicitly call navigate('/gc-test') and assert window.goatcounter.count was not
called (verifying the skip behavior), then perform the second navigate and
assert window.goatcounter.count was called with an objectContaining both { path:
'/gc-test', title: 'GC Test' } (or the expected document/title value used by
your router); reference the existing helpers addRoute and navigate and the mock
window.goatcounter.count when adding these assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 57f6ec3f-59bd-4931-a98e-75c17bcaebba

📥 Commits

Reviewing files that changed from the base of the PR and between 90ee697 and 57f7a13.

📒 Files selected for processing (2)
  • website/src/utils/router.js
  • website/src/utils/router.test.js

@rdmueller rdmueller merged commit a941c64 into LLM-Coding:main Jun 2, 2026
7 checks passed
raifdmueller added a commit to raifdmueller/Semantic-Anchors that referenced this pull request Jun 2, 2026
document.referrer is fixed for the whole SPA session, so every in-app
pageview we send (LLM-Coding#562/LLM-Coding#565) re-credited the entry referrer — e.g. a single
visitor arriving from heise and browsing 20 anchors made heise's referrer
list show all 20 paths. Report the referrer only on the initial load (handled
by count.js); client-side route changes now send an empty referrer via a
callback. External referrers are credited to the landing page only, matching
the intuitive "where did this visit come from" meaning.

Verified with GoatCounter's get_data: with document.referrer = heise, the
landing keeps r="https://www.heise.de" while an SPA anchor view yields r="".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants